import type { Server } from 'http'
import WebSocket, { WebSocketServer } from 'ws'
import { defaultStorage as storage } from '../infra/storage.mjs'
import { DeviceConfigMessage } from '../types/device-config.mjs'
import { logger } from '../infra/logger.mjs'

interface DeviceConnection {
  socket: WebSocket
  deviceId: string
  lastHeartbeatAt: number
}

const PING_INTERVAL_MS = 30_000
const HEARTBEAT_TIMEOUT_MS = 60_000

class DeviceGateway {
  private wss?: WebSocketServer
  private readonly connections = new Map<string, DeviceConnection>()
  private pingTimer?: NodeJS.Timeout

  start(server: Server): void {
    if (this.wss) {
      return
    }

    this.wss = new WebSocketServer({ server, path: '/ws/device' })
    this.wss.on('connection', (socket: WebSocket) => this.handleConnection(socket))

    this.pingTimer = setInterval(() => this.pingClients(), PING_INTERVAL_MS)
    logger.info('DeviceGateway_started')
  }

  sendConfig(deviceId: string, configs: DeviceConfigMessage[]): boolean {
    const connection = this.connections.get(deviceId)
    if (!connection) {
      return false
    }

    try {
      connection.socket.send(JSON.stringify({
        type: 'CONFIG',
        payload: {
          configs
        }
      }))
      return true
    } catch (error) {
      logger.error('DeviceGateway_sendConfig_error', error as Error, { deviceId })
      return false
    }
  }

  private handleConnection(socket: WebSocket): void {
    let registeredDeviceId: string | null = null

    const onMessage = async (data: WebSocket.RawData) => {
      try {
        const parsed = JSON.parse(data.toString())
        const messageType = parsed?.type

        if (!registeredDeviceId) {
          if (messageType !== 'HANDSHAKE') {
            socket.close(4000, 'Handshake required')
            return
          }
          const deviceId = typeof parsed?.payload?.deviceId === 'string' ? parsed.payload.deviceId.trim() : ''
          if (!deviceId) {
            socket.close(4001, 'Invalid deviceId')
            return
          }

          registeredDeviceId = deviceId
          await this.registerDeviceConnection(deviceId, socket)
          socket.send(JSON.stringify({ type: 'HANDSHAKE' }))
          logger.info('DeviceGateway_handshake_success', { deviceId })
          return
        }

        await this.handleDeviceMessage(registeredDeviceId, parsed)
      } catch (error) {
        logger.error('DeviceGateway_message_parse_error', error as Error)
      }
    }

    const onClose = async () => {
      if (registeredDeviceId) {
        await this.handleDisconnection(registeredDeviceId, socket)
      }
    }

    socket.on('message', onMessage)
    socket.on('close', onClose)
    socket.on('error', (error: Error) => {
      logger.error('DeviceGateway_socket_error', error as Error, { deviceId: registeredDeviceId ?? undefined })
    })
  }

  private async registerDeviceConnection(deviceId: string, socket: WebSocket): Promise<void> {
    const existing = this.connections.get(deviceId)
    if (existing) {
      existing.socket.close(4002, 'New connection established')
    }

    const device = await storage.ensureDevice(deviceId)
    await storage.markDeviceOnline(device.deviceId)

    this.connections.set(deviceId, {
      socket,
      deviceId,
      lastHeartbeatAt: Date.now()
    })
  }

  private async handleDeviceMessage(deviceId: string, message: any): Promise<void> {
    const type = message?.type
    switch (type) {
      case 'PONG': {
        this.touchDevice(deviceId)
        await storage.markDeviceOnline(deviceId)
        break
      }
      case 'UPLOAD_ALL_CONFIG': {
        this.touchDevice(deviceId)
        await storage.markDeviceOnline(deviceId)
        if (Array.isArray(message?.payload?.configs)) {
          const device = await storage.findDeviceByDeviceIdentifier(deviceId)
          if (device) {
            await storage.upsertDeviceConfig(device, message.payload.configs as DeviceConfigMessage[], 'DEVICE')
            await storage.updateDeviceLastStatus(deviceId, { online: true })
          }
        }
        break
      }
      default: {
        logger.warn('DeviceGateway_unknown_message_type', { deviceId, type })
      }
    }
  }

  private async handleDisconnection(deviceId: string, socket: WebSocket): Promise<void> {
    const current = this.connections.get(deviceId)
    if (current?.socket === socket) {
      this.connections.delete(deviceId)
      await storage.markDeviceOffline(deviceId)
      logger.info('DeviceGateway_disconnected', { deviceId })
    }
  }

  private pingClients(): void {
    const now = Date.now()
    for (const [deviceId, connection] of this.connections.entries()) {
      if (now - connection.lastHeartbeatAt > HEARTBEAT_TIMEOUT_MS) {
        logger.warn('DeviceGateway_heartbeat_timeout', { deviceId })
        connection.socket.terminate()
        this.connections.delete(deviceId)
        storage.markDeviceOffline(deviceId).catch((error) => {
          logger.error('DeviceGateway_mark_offline_error', error as Error, { deviceId })
        })
        continue
      }

      try {
        connection.socket.send(JSON.stringify({ type: 'PING' }))
      } catch (error) {
        logger.error('DeviceGateway_ping_error', error as Error, { deviceId })
      }
    }
  }

  private touchDevice(deviceId: string): void {
    const connection = this.connections.get(deviceId)
    if (connection) {
      connection.lastHeartbeatAt = Date.now()
    }
  }
}

export const deviceGateway = new DeviceGateway()

